using Unity.Mathematics;
using EarthUtility;
using Geometry;

namespace EarthGeospatial
{
    public class GlobeRectangle
    {
        double _west;
        double _south;
        double _east;
        double _north;

        /**
       * @brief Constructs a new instance.
       *
       * @param west The westernmost longitude, in radians, in the range [-Pi, Pi].
       * @param south The southernmost latitude, in radians, in the range [-Pi/2,
       * Pi/2].
       * @param east The easternmost longitude, in radians, in the range [-Pi, Pi].
       * @param north The northernmost latitude, in radians, in the range [-Pi/2,
       * Pi/2].
       */
        public GlobeRectangle(
            double west,
            double south,
            double east,
            double north)
        {
            this._west = west;
            this._south = south;
            this._east = east;
            this._north = north;
        }


        /**
         * Creates a rectangle given the boundary longitude and latitude in degrees.
         * The angles are converted to radians.
         *
         * @param westDegrees The westernmost longitude in degrees in the range
         * [-180.0, 180.0].
         * @param southDegrees The southernmost latitude in degrees in the range
         * [-90.0, 90.0].
         * @param eastDegrees The easternmost longitude in degrees in the range
         * [-180.0, 180.0].
         * @param northDegrees The northernmost latitude in degrees in the range
         * [-90.0, 90.0].
         * @returns The rectangle.
         *
         * @snippet TestGlobeRectangle.cpp fromDegrees
         */
        public static GlobeRectangle fromDegrees(
              double westDegrees,
              double southDegrees,
              double eastDegrees,
              double northDegrees)
        {
            return new GlobeRectangle(
                Mathd.degreesToRadians(westDegrees),
                Mathd.degreesToRadians(southDegrees),
                Mathd.degreesToRadians(eastDegrees),
                Mathd.degreesToRadians(northDegrees));
        }

        /**
         * @brief Returns the westernmost longitude, in radians.
         */
        public double getWest() { return this._west; }

        /**
         * @brief Returns the southernmost latitude, in radians.
         */
        public double getSouth() { return this._south; }

        /**
         * @brief Returns the easternmost longitude, in radians.
         */
        public double getEast() { return this._east; }

        /**
         * @brief Returns the northernmost latitude, in radians.
         */
       public double getNorth() { return this._north; }

        /**
         * @brief Returns the {@link Cartographic} position of the south-west corner.
         */
        public Cartographic getSouthwest()
        {
            return new Cartographic(this._west, this._south);
        }

        /**
         * @brief Returns the {@link Cartographic} position of the south-east corner.
         */
        public Cartographic getSoutheast()
        {
            return new Cartographic(this._east, this._south);
        }

        /**
         * @brief Returns the {@link Cartographic} position of the north-west corner.
         */
        public Cartographic getNorthwest()
        {
            return new Cartographic(this._west, this._north);
        }

        /**
         * @brief Returns the {@link Cartographic} position of the north-east corner.
         */
        public Cartographic getNortheast()
        {
            return new Cartographic(this._east, this._north);
        }

        /**
         * @brief Returns this rectangle as a {@link CesiumGeometry::Rectangle}.
         */
        public Rectangle toSimpleRectangle()
        {
            return new Rectangle(
                this.getWest(),
                this.getSouth(),
                this.getEast(),
                this.getNorth());
        }

        /**
         * @brief Computes the width of this rectangle.
         *
         * The result will be in radians, in the range [0, Pi*2].
         */
        public double computeWidth()
        {
            double east = this._east;
            double west = this._west;
            if (east < west)
            {
                east += Mathd.TWO_PI;
            }
            return east - west;
        }

        /**
         * @brief Computes the height of this rectangle.
         *
         * The result will be in radians, in the range [0, Pi*2].
         */
        double computeHeight()
        {
            return this._north - this._south;
        }

        /**
         * @brief Computes the {@link Cartographic} center position of this rectangle.
         */
        public Cartographic computeCenter()
        {
            double east = this._east;
            double west = this._west;

            if (east < west)
            {
                east += Mathd.TWO_PI;
            }

            double longitude = Mathd.negativePiToPi((west + east) * 0.5);
            double latitude = (this._south + this._north) * 0.5;

            return new Cartographic(longitude, latitude, 0.0);
        }

        /**
         * @brief Returns `true` if this rectangle contains the given point.
         *
         * This will take into account the wrapping of the latitude and longitude,
         * and also return `true` when the longitude of the given point is within
         * a very small error margin of the longitudes of this rectangle.
         */
        public bool contains(Cartographic cartographic)
        {
            double longitude = cartographic.longitude;
            double latitude = cartographic.latitude;

            double west = this._west;
            double east = this._east;

            if (east < west)
            {
                east += Mathd.TWO_PI;
                if (longitude < 0.0)
                {
                    longitude += Mathd.TWO_PI;
                }
            }
            return (
                (longitude > west ||
                Mathd.equalsEpsilon(longitude, west, Mathd.EPSILON14)) &&
                (longitude < east ||
                Mathd.equalsEpsilon(longitude, east, Mathd.EPSILON14)) &&
                latitude >= this._south && latitude <= this._north);
        }

        /**
         * @brief Computes the intersection of two rectangles.
         *
         * This function assumes that the rectangle's coordinates are latitude and
         * longitude in radians and produces a correct intersection, taking into
         * account the fact that the same angle can be represented with multiple
         * values as well as the wrapping of longitude at the anti-meridian.  For a
         * simple intersection that ignores these factors and can be used with
         * projected coordinates, see
         * {@link CesiumGeometry::Rectangle::computeIntersection}.
         *
         * @param other The other rectangle to intersect with this one.
         * @returns The intersection rectangle, or `std::nullopt` if there is no
         * intersection.
         */
        public GlobeRectangle computeIntersection(GlobeRectangle other)
        {
            double rectangleEast = this._east;
            double rectangleWest = this._west;

            double otherRectangleEast = other._east;
            double otherRectangleWest = other._west;

            if (rectangleEast < rectangleWest && otherRectangleEast > 0.0)
            {
                rectangleEast += Mathd.TWO_PI;
            }
            else if (otherRectangleEast < otherRectangleWest && rectangleEast > 0.0)
            {
                otherRectangleEast += Mathd.TWO_PI;
            }

            if (rectangleEast < rectangleWest && otherRectangleWest < 0.0)
            {
                otherRectangleWest += Mathd.TWO_PI;
            }
            else if (otherRectangleEast < otherRectangleWest && rectangleWest < 0.0)
            {
                rectangleWest += Mathd.TWO_PI;
            }

            double west = Mathd.negativePiToPi(
               math.max(rectangleWest, otherRectangleWest));
            double east = Mathd.negativePiToPi(
               math.min(rectangleEast, otherRectangleEast));

            if ((this._west < this._east || other._west < other._east) &&
                east <= west)
            {
                return null;
            }

            double south = math.max(this._south, other._south);
            double north = math.min(this._north, other._north);

            if (south >= north)
            {
                return null;
            }

            return new GlobeRectangle(west, south, east, north);
        }

        /**
         * @brief Computes the union of this globe rectangle with another.
         *
         * @param other The other globe rectangle.
         * @return The union.
         */
        public GlobeRectangle computeUnion(GlobeRectangle other)
        {
            double rectangleEast = this._east;
            double rectangleWest = this._west;

            double otherRectangleEast = other._east;
            double otherRectangleWest = other._west;

            if (rectangleEast < rectangleWest && otherRectangleEast > 0.0)
            {
                rectangleEast += Mathd.TWO_PI;
            }
            else if (otherRectangleEast < otherRectangleWest && rectangleEast > 0.0)
            {
                otherRectangleEast += Mathd.TWO_PI;
            }

            if (rectangleEast < rectangleWest && otherRectangleWest < 0.0)
            {
                otherRectangleWest += Mathd.TWO_PI;
            }
            else if (otherRectangleEast < otherRectangleWest && rectangleWest < 0.0)
            {
                rectangleWest += Mathd.TWO_PI;
            }

            double west = Mathd.convertLongitudeRange(
               math.min(rectangleWest, otherRectangleWest));
            double east = Mathd.convertLongitudeRange(
               math.max(rectangleEast, otherRectangleEast));

            return new GlobeRectangle(
                west,
                math.min(this._south, other._south),
                east,
                math.max(this._north, other._north));

        }

        public static implicit operator bool(GlobeRectangle foo)
        {
            return foo == null ? false : true;
        }

    }
}